<?php


namespace BoojasrFramework\ServicesContainer;


use BoojasrFramework\ServicesContainer\DependencyInjection\Compiler\CompilerPassInterface;
use BoojasrFramework\ServicesContainer\DependencyInjection\Compiler\PassContainer;
use BoojasrFramework\ServicesContainer\Entity\Definition;
use Symfony\Component\Config\Resource\ComposerResource;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\GlobResource;
use Symfony\Component\Config\Resource\ResourceInterface;

class ContainerBuilder extends Container
{
    /**
     * @var Definition[]
     */
    private $definitions = [];

    /**
     * @var Definition[]
     */
    private $classIdDefinitions = [];

    /**
     * @var ResourceInterface[]
     */
    private $resources = [];

    private $usedTags = [];

    private $trackResources;

    /**
     * @var string[] the list of vendor directories
     */
    private $vendors;

    public function __construct()
    {
        $this->trackResources = interface_exists(ResourceInterface::class);
    }

    /**
     * @return Definition
     */
    public function getDefinition($id)
    {
        if (isset($this->definitions[$id])) return $this->definitions[$id];
        if (isset($this->classIdDefinitions[$id])) return $this->classIdDefinitions[$id];
        return null;
    }

    /**
     * @return array
     */
    public function getDefinitions(){
        return $this->definitions;
    }

    /**
     * @param $id
     * @return bool
     */
    public function hasDefinition($id){
        return isset($this->definitions[$id])||isset($this->classIdDefinitions[$id]);
    }

    /**
     * @param Definition[] $definitions
     * @return $this
     */
    public function addDefinitions($definitions)
    {
        $this->definitions = array_merge($this->definitions,$definitions);
        foreach ($this->definitions as $definition){
            $this->classIdDefinitions[$definition->getClass()] = $definition;
        }
        return $this;
    }

    /**
     * @param $id
     * @param Definition $definition
     * @return $this
     */
    public function setDefinition($id,$definition){
        $this->definitions[$id] = $definition;

        return $this;
    }

    public function addCompilerPass(CompilerPassInterface $pass, $type = PassContainer::TYPE_BEFORE_OPTIMIZATION, $priority = 0)
    {
        $this->getCompiler()->addPass($pass, $type, $priority);

        return $this;
    }

    public function getCompilerPassConfig()
    {
        return $this->getCompiler()->getPassContainer();
    }

    public function findTaggedServiceIds( $name, $throwOnAbstract = false)
    {
        $this->usedTags[] = $name;
        $tags = [];
        foreach ($this->getDefinitions() as $id => $definition) {
            if ($definition->hasTag($name)) {
                if ($throwOnAbstract && $definition->isAbstract()) {
                    throw new \InvalidArgumentException(sprintf('The service "%s" tagged "%s" must not be abstract.', $id, $name));
                }
                $tags[$id] = $definition->getTag($name);
            }
        }

        return $tags;
    }
    public function getReflectionClass($class){
        if ($class){
            return new \ReflectionClass($class);
        }
        return null;
    }

    /**
     * @return ResourceInterface[]
     */
    public function getResources(): array
    {
        return $this->resources;
    }

    /**
     * @param ResourceInterface[] $resources
     */
    public function setResources(array $resources): void
    {
        $this->resources = $resources;
    }

    /**
     * @return $this
     */
    public function addResource(ResourceInterface $resource)
    {
        if (!$this->trackResources) {
            return $this;
        }

        if ($resource instanceof GlobResource && $this->inVendors($resource->getPrefix())) {
            return $this;
        }

        $this->resources[(string) $resource] = $resource;

        return $this;
    }

    /**
     * Sets the track resources flag.
     *
     * If you are not using the loaders and therefore don't want
     * to depend on the Config component, set this flag to false.
     *
     * @param bool $track True if you want to track resources, false otherwise
     */
    public function setResourceTracking($track)
    {
        $this->trackResources = (bool) $track;
    }

    /**
     * Checks if resources are tracked.
     *
     * @return bool true If resources are tracked, false otherwise
     */
    public function isTrackingResources()
    {
        return $this->trackResources;
    }

    private function inVendors(string $path): bool
    {
        if (null === $this->vendors) {
            $this->vendors = (new ComposerResource())->getVendors();
        }
        $path = realpath($path) ?: $path;

        foreach ($this->vendors as $vendor) {
            if (str_starts_with($path, $vendor) && false !== strpbrk(substr($path, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) {
                $this->addResource(new FileResource($vendor.'/composer/installed.json'));

                return true;
            }
        }

        return false;
    }

}
